# BigData course project
# Serial version of training algorithm for SOM
# Neuron class

import random
from math import *
from util import *

class Neuron:
    # constructor for the neuron, initializes randomly the weights
    def __init__(self, dim, min_w, max_w, x, y):
        self.x = x
        self.y = y
        rand = lambda: (max_w-min_w)*random.random() + min_w
        self.w = [rand() for i in xrange(dim)]
        self.d = float(0)
        
    def dump(self):
        deb("neuron at [%d,%d]\n", int(self.x), int(self.y));
        for i in xrange(len(self.w)):
            deb("%.6f ", self.w[i])
        deb("\n")

    # the learning rate (called alpha in formula); decreases
    # exponentially with iteration (t). it also needs to know the
    # total number of iterations (max_t)
    @staticmethod    
    def _alpha(t, max_t):
        return 0.5 * exp(-float(t) / float(max_t))

    # the neighbourhood ratio (called sigma in formula); which
    # decreases exponentially with iteration (t). it also needs to know
    # the r,max_r parameters of the som
    @staticmethod
    def _sigma(t, r, max_r):
        return r * exp(-float(t) / max_r);

    # euclidean distance in the map (mesh), between this neuron and 
    # neuron v (thought as the winner of current iteration)
    def _dist_map(self, v):
        return sqrt((self.x - v.x)**2 + (self.y - v.y)**2)

    # calculates the influence (eta in formula) of a neuron (self)
    # respect to the other (v), where the second is thought as the center
    # of a neighbourhood. it requires to know also current iteration time
    # (t), as well as the som parameters (r and max_r)
    def _eta(self, t, v, r, max_r):
        return exp(-(self._dist_map(v)**2) / (2 * Neuron._sigma(t, r, max_r)))

    # given the winning neuron v, updates the weights of neuron (self)
    # needs to know iteration (t), and maximum number of iters (max_t),
    # as well as the som params (r, max_r and dw)
    def update_weights(self, t, v, max_t, r, max_r, dw):
        alpha_eta = Neuron._alpha(t, max_t) * self._eta(t,v,r,max_r)
        for j in xrange(len(self.w)):
            self.w[j] += alpha_eta * dw[j]

    # euclidean distance between the i-th training vector and the given
    # neuron's weight vector. also updates internal cache for the distance
    def dist(self, ts, i):
        d2 = sum([(ts.c[i].get(j) - self.w[j])**2 for j in xrange(ts.dim)])
        self.d = sqrt(d2)
        return self.d
